#! /usr/bin/perl -w

# click-elem2man -- creates man pages from structured comments in element
# source code
# Eddie Kohler
# Robert Morris - original make-faction-html script
#
# Copyright (c) 1999-2001 Massachusetts Institute of Technology
# Copyright (c) 2001-2003 International Computer Science Institute
# Copyright (c) 2006-2007 Regents of the University of California
# Copyright (c) 2011 Meraki, Inc.
# Copyright (c) 2012-2013 Eddie Kohler
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, subject to the conditions
# listed in the Click LICENSE file. These conditions include: you must
# preserve this copyright notice, and you cannot mention the copyright
# holders in advertising related to the Software without their permission.
# The Software is provided WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED. This
# notice is a summary of the Click LICENSE file; the license in that file is
# legally binding.

my($verbose) = 0;

# information stored about each Click element, indexed by C++ class name
my(%processing, %deprecated, %portcount, %batching, %docrequires, %requires, $PREFIX);

my(%section_break) =
    ( 'head1' => 1, 'c' => 1, 's' => 1,
      'io' => 1, 'processing' => 1, 'drivers' => 1, 'batching' => 1,
      'd' => 1, 'n' => 1, 'e' => 1, 'h' => 1, 'a' => 1, 'title' => 1,
      'deprecated' => 1, 'package' => 1 );
my(%section_takes_args) =
    ( 'head1' => 1, 'c' => 0, 's' => 2,
      'io' => 2, 'processing' => 2, 'drivers' => 2, 'batching' => 2,
      'd' => 0, 'n' => 0, 'e' => 0, 'h' => 1, 'a' => 2, 'title' => 1,
      'deprecated' => 1, 'package' => 2 );
my(%section_takes_text) =
    ( 'head1' => 2, 'c' => 1, 's' => 2,
      'io' => 2, 'processing' => 2, 'drivers' => 2, 'batching' => 2,
      'd' => 2, 'n' => 1, 'e' => 1, 'h' => 2, 'a' => 2, 'title' => 0,
      'deprecated' => 0, 'package' => 1 );
my(%xsection_takes_args) =
    ( 'head1' => 1, 'head2' => 1, 'item' => 1, 'over' => 1, 'back' => 0,
      'for' => 1, 'begin' => 1, 'end' => 1, 'start!' => 1, 'name!' => 1,
      'end!' => 0, 'text' => 0 );
my(%infosection_name) =
    ( 'package' => 'Package', 'io' => 'Ports', 'processing' => 'Processing',
      'batching' => 'Batching', 'drivers' => 'Drivers' );

my $man_section = "7click";
my $man_linksection = "7";
my $package;
my $gzip = undef;
my $uninstall = 0;
my(@all_outnames, %all_outsections, %summaries_by_section, %all_summaries);
my(%el_generated);

my(%processing_constants) =
    ( 'AGNOSTIC' => 'a/a', 'PUSH' => 'h/h', 'PULL' => 'l/l',
      'PUSH_TO_PULL' => 'h/l', 'PULL_TO_PUSH' => 'l/h',
      'PROCESSING_A_AH' => 'a/ah', 'PROCESSING_H_LH' => 'h/lh' );
my(%processing_text) =
    ( 'a/a' => 'agnostic', 'h/h' => 'push', 'l/l' => 'pull',
      'h/l' => 'push inputs, pull outputs',
      'l/h' => 'pull inputs, push outputs',
      'a/ah' => 'agnostic, but output 1 is push' );

my(@section_headers) =
    ( 'basicsources'            => 'Basic Sources and Sinks',
      'classification'          => 'Basic Classification and Selection',
      'basictransfer'           => 'Basic Packet Transfer',
      'counters'                => 'Counters',
      'ctx'                     => 'Context Sybsystem',
      'flow'                    => 'Flow Subsystem',
      'timestamps'              => 'Timestamps',
      'basicmod'                => 'Basic Packet Modification',
      'storage'                 => 'Packet Storage',
      'aqm'                     => 'Active Queue Management',
      'scheduling'              => 'Packet Scheduling',
      'shaping'                 => 'Traffic Shaping',
      'information'             => 'Information Elements',
      'netdevices'              => 'Network Devices',
      'comm'                    => 'Host and Socket Communication',
      'ethernet'                => 'Ethernet',
      'arp'                     => 'ARP',
      'ip'                      => 'IPv4',
      'iproute'                 => 'IPv4 Routing',
      'icmp'                    => 'ICMP',
      'nat'                     => 'Network Address Translation',
      'tcp'                     => 'TCP',
      'udp'                     => 'UDP',
      'tunnel'                  => 'Tunnel (GRE, ...)',
      'app'                     => 'Applications',
      'traces'                  => 'Trace Manipulation',
      'ipmeasure'               => 'TCP/IP Measurement',
      'aggregates'              => 'Aggregates',
      'ip6'                     => 'IPv6',
      'ipsec'                   => 'IPsec',
      'crc'                     => 'CRCs',
      'paint'                   => 'Paint Annotations',
      'research'                => 'Research elements',
      'annotations'             => 'Annotations',
      'debugging'               => 'Debugging',
      'control'                 => 'Control',
      'threads'                 => 'Thread Management',
      'tunnel'                  => 'Tunneling',
      'test'                    => 'Regression Tests'
      );
my(%section_headers);
for (my $i = 0; $i < @section_headers; $i += 2) {
    $section_headers{$section_headers[$i]} = $section_headers[$i+1];
}

my(%default_packages) =
    ( 'analysis'                => 'analysis (core)',
      'app'                     => 'app (core)',
      'aqm'                     => 'aqm (core)',
      'bsdmodule'               => 'bsdmodule (core)',
      'ctx'                     => 'ctx (core)',
      'ethernet'                => 'ethernet (core)',
      'etherswitch'             => 'etherswitch (core)',
      'flow'                    => 'flow (core)',
      'grid'                    => 'grid (core)',
      'icmp'                    => 'icmp (core)',
      'ip'                      => 'ip (core)',
      'ip6'                     => 'ip6 (core)',
      'ipsec'                   => 'ipsec (core)',
      'linuxmodule'             => 'linuxmodule (core)',
      'local'                   => 'local (core)',
      'ns'                      => 'ns (core)',
      'radio'                   => 'radio (core)',
      'research'                => 'research (core)',
      'simple'                  => 'simple (core)',
      'standard'                => 'standard (core)',
      'tunnel'                  => 'tunnel (core)',
      'tcpudp'                  => 'tcpudp (core)',
      'test'                    => 'test (core)',
      'userlevel'               => 'userlevel (core)',
      'wifi'                    => 'wifi (core)'
      );

# find date
my(@today) = localtime;
my($today) = sprintf("%d/%02d/%02d", $today[5] + 1900, $today[4] + 1, $today[3]);

# Unrolling [^A-Z>]|[A-Z](?!<) gives:    // MRE pp 165.
my $nonest = '(?:[^A-Z>]*(?:[A-Z](?!<)[^A-Z>]*)*)';


# XXX one paragraph at a time

my($Filename, $PrimaryElement, @Related, %RelatedSource, @Over, $Begun, $Lineno, $Github);


############
## shared ##

sub textize_add_links ($$) {
    my($t, $selfref) = @_;
    my($pos);

    # embolden & manpageize
    foreach my $r (@Related) {
        my $l = length($r);
        my $result = ($r eq $PrimaryElement ? "$selfref<$r>" : "L<$r>");
        for ($pos = index($t, $r); $pos >= 0; $pos = index($t, $r, $pos + 1)) {
            if (($pos == 0 || substr($t, $pos - 1, 1) =~ /^[^\w@\/<]$/s)
                && (substr($t, $pos + $l) =~ /\A(?:[^\w@\/]|\z)/s)) {
                substr($t, $pos, $l) = $result;
                $pos += 2 + $l;
            }
        }
    }
    $t;
}

sub quote_unquoted_gt ($) {
    my($t) = @_;
    my($tt, $nest, $pos) = ('', 0);
    for ($pos = index($t, ">"); $pos >= 0; $pos = index($t, ">")) {
        my($w) = substr($t, 0, $pos);
        $nest++ while $w =~ m|[A-Z]<|g;
        if ($nest) {
            $tt .= $w . ">";
            $nest--;
        } else {
            $tt .= $w . "E<gt>";
        }
        $t = substr($t, $pos + 1);
    }
    $tt . $t;
}


###########
## nroff ##

my $nroff_prologue = <<'EOD;';
.de M
.IR "\\$1" "(\\$2)\\$3"
..
.de RM
.RI "\\$1" "\\$2" "(\\$3)\\$4"
..
EOD;
chomp $nroff_prologue;

my(%nroff_podentities) =
    ( 'lt' => '<', 'gt' => '>', 'amp' => '&', 'solid' => '/',
      'verbar' => '|', 'eq' => '=', 'star' => '*', 'lparen' => '(',
      'rparen' => ')', 'lbrack' => '[', 'rbrack' => ']', 'lbrace' => '{',
      'rbrace' => '}' );

sub nroff_quote ($) {
    my($x) = @_;
    $x =~ tr/\000-\177/\200-\377/;
    $x;
}

sub nroff_unentity ($) {
    my($x) = @_;
    $x =~ tr/\200-\377/\000-\177/;
    if ($x =~ /^\d+$/) {
        chr($x);
    } elsif ($nroff_podentities{$x}) {
        $nroff_podentities{$x};
    } else {
        print STDERR "click-elem2man: $Filename: unknown entity E<$x>\n";
        "";
    }
}

sub nroff_fixfP ($$) {
    my($x, $f) = @_;
    $x =~ s/\\fP/\\f$f/g;
    $x;
}

sub nroff_manref ($$$$) {
    my(@a) = @_;
    map { $_ = "\"$_\"" if (s/\"/\\(dq/g) } @a;
    my($x);
    if ($a[0] eq "") {
        $x = "\n.M " . $a[1] . " " . $a[2];
    } else {
        $x = "\n.RM " . $a[0] . " " . $a[1] . " " . $a[2];
    }
    $x .= " " . $a[3] if $a[3] ne "";
    $x . "\n";
}

sub nroffize_text ($) {
    my($t) = @_;
    my($s);

    # embolden & manpageize
    $t = textize_add_links($t, 'L');

    $t =~ s/\\/\\\\/g;
    $t =~ s/^\./\\&./gm;
    $t =~ s/^'/\\&'/gm;
    $t =~ s/^\s*$/.PP\n/gm;
    $t =~ s/^\.PP\n(?:\.PP\n)+/.PP\n/gm;
    $t =~ s{\A\s*(?:\.PP\n)?}{}s;
    if (@Over > 0) {
        $t =~ s/^\.PP$/.IP \"\" $Over[-1]/gm;
    }

    # get rid of entities
    $t =~ s{[\200-\377]}{"E<" . ord($1) . ">"}ge;
    $t =~ s{(E<[^>]*>)}{nroff_quote($1)}ge;

    my $maxnest = 10;
    while ($maxnest-- && $t =~ /[A-Z]</) {

        # can't do C font here
        $t =~ s/([BI])<($nonest)>/"\\f$1" . nroff_fixfP($2, $1) . "\\fP"/eg;
        $t =~ s/[PU]<($nonest)>/$1/g;

        # files and filelike refs in italics
        $t =~ s/F<($nonest)>/I<$1>/g;

        # comments
        $t =~ s/V<($nonest)>//g;

        # LREF: man page references, or a la HREF L<show this text|man/section>
        $t =~ s{(\S*)L<($nonest)>(\S*)(\s*)}{
            my($pre, $text, $post, $space, $s, $i) = ($1, $2, $3, $4);
            if ($text =~ /\A(.*)\((\d+|[ln])\)\z/) {
                nroff_manref($pre, $1, $2, $post);
            } elsif (($s = $RelatedSource{$text})) {
                nroff_manref($pre, $text, $s, $post);
            } elsif (($i = index($text, "|")) >= 0) {
                $pre . substr($text, 0, $i) . $post . $space;
            } else {
                $pre . "\\fB" . $text . "\\fP" . $post . $space;
            }
        }eg;

        $t =~ s/Z<>/\\&/g;
        $t =~ s/N<>(\n?)/\n.br\n/g;

        # comes last because not subject to reprocessing
        $t =~ s/C<($nonest)>/"\\f(CW" . nroff_fixfP($1, 'R') . "\\fP"/eg;
    }

    # replace entities
    $t =~ s/\305\274([^\276]*)\276/nroff_unentity($1)/ge;

    # fix fonts
    $t =~ s/\\fP/\\fR/g;

    # fix manual
    $t =~ s/\n\.M (\S+) \"(\S+)\" (\(\2\))/\n.M $1 \"$2\" /sg;

    $t =~ s/\n+/\n/sg;
    $t =~ s/^\n+//;
    $t =~ s/\n+$//;

    $t;
}

sub nroffize ($) {
    my($t) = @_;
    # $t cannot contain \t, as all \ts have been expanded to spaces below

    #$t =~ s{\n[ \r]+$}{\n}gm;

    if ($t =~ /^ /m) {
        # worry about verbatims
        $t =~ s/^( .*)\n(\n+)(?= )/$1 . "\n" . (" \n" x length($2))/mge;
        my(@x) = split(/(^ .*$)/m, $t);
        my($o, $i) = '';
        for ($i = 0; $i < @x; $i += 2) {
            if ($x[$i]) {
                $o .= nroffize_text($x[$i]) . "\n";
            }
            if ($x[$i+1]) {
                $x[$i+1] =~ s/\\/\\e/g;
                $o .= ".nf\n\\&" . $x[$i+1] . "\n.fi\n.PP\n";
            }
        }
        $o =~ s/\n\.fi\n\.PP\n\n\.nf\n/\n/g;
        if (@Over > 0) {
            $o =~ s/^\.PP$/.IP \"\" $Over[-1]/gm;
        }
        $o;
    } else {
        nroffize_text($t);
    }
}

sub protquote ($) {
    my($t) = @_;
    $t =~ s/\"/\\(dq/g;
    $t;
}

sub nroff_do_section ($$$$) {
    my($name, $args, $text, $filename) = @_;
    if (!exists($xsection_takes_args{$name})) {
        print STDERR "click-elem2man: $Filename: unknown section '=$name' ignored\n";
        return;
    }
    print STDERR "click-elem2man: $Filename: section '=$name' requires arguments\n"
        if ($xsection_takes_args{$name} && !$args);

    # handle '=begin' .. '=end'
    if ($name eq 'end') {
        undef $Begun;
    } elsif ($Begun && ($Begun eq 'man' || $Begun eq 'roff')) {
        print OUT '=', $name, ($args ? ' ' . $args : ''), "\n", $text;
        return;
    } elsif ($Begun) {
        return;
    }

    if ($name eq 'head1') {
        print OUT ".SH \"", protquote(nroffize($args)), "\"\n";
    } elsif ($name eq 'head2') {
        print OUT ".SS \"", protquote(nroffize($args)), "\"\n";
    } elsif ($name eq 'over') {
        if ($args =~ /^\s*(\d+)\s*$/s) {
            print OUT ".RS $Over[-1]\n" if @Over;
            push @Over, $1;
        } else {
            print STDERR "click-elem2man: $Filename: bad arguments to '=over'\n";
        }
    } elsif ($name eq 'item') {
        my($over) = (@Over ? $Over[-1] : 8);
        print OUT ".IP \"", protquote(nroffize($args)), "\" $over\n";
    } elsif ($name eq 'back') {
        my($overarg);
        if ($args =~ /^\s*(\d+)\s*$/s) {
            $overarg = $1;
        } elsif ($args !~ /^\s*$/s) {
            print STDERR "click-elem2man: $Filename: bad arguments to '=back'\n";
        }
        if (@Over == 0) {
            print STDERR "click-elem2man: $Filename: too many '=back's\n";
        } else {
            my($over) = pop @Over;
            print OUT ".RE\n" if @Over;
            print OUT (@Over ? ".IP \"\" $Over[-1]\n" : ".PP\n");
            print STDERR "click-elem2man: $Filename: '=back $overarg' paired with '=over $over'\n"
                if defined($overarg) && $over != $overarg;
        }
    } elsif ($name eq 'for') {
        my($fortext);
        if ($text =~ /^(.*)\n\s*\n(.*)$/s) {
            ($fortext, $text) = ($1, $2);
        } else {
            ($fortext, $text) = ($text, '');
        }
        if ($args =~ /^\s*(man|roff)\s*(.*)/) {
            print OUT $2, $fortext;
        }
    } elsif ($name eq 'begin') {
        $Begun = $args;
        $Begun =~ s/^\s*(\S+).*/$1/;
        if ($Begun eq 'man' || $Begun eq 'roff') {
            print OUT $text;
        }
        return;
    } elsif ($name eq 'start!') {
        print OUT <<"EOD;";
.\\" -*- mode: nroff -*-
.\\" Generated by 'click-elem2man' from '$Filename'
$nroff_prologue
.TH "\U$text\E" $args "$today" "Click"
EOD;
        return;
    } elsif ($name eq 'name!') {
        print OUT <<"EOD;";
.SH "NAME"
$args \\- $text
EOD;
        return;
    } elsif ($name eq 'end!') {
        return;
    }

    print OUT nroffize($text), "\n";
    print OUT "\n" if $name eq 'head1';
}

sub nroff_elementlist ($@) {
    my($sec) = shift @_;
    my($t);
    if ($sec eq "Alphabetical") {
        $t = ".SH \"ALPHABETICAL LIST\"\n";
    } else {
        $t = ".SS \"$sec\"\n";
    }
    $t .= ".PP\n.PD 0\n";
    foreach $_ (sort { lc($a) cmp lc($b) } @_) {
        my($os) = $all_outsections{$_};
        $os =~ s/\A(\d)[a-z]+\z/$1/;
        $t .= ".TP 20\n.M " . $_ . " " . $os;
        $t .= " \" (deprecated)\"" if $deprecated{$_};
        $t .= "\n";
        $t .= $all_summaries{$_} if $all_summaries{$_} =~ /\S/;
        $el_generated{$_} = 1;
    }
    $t . ".PD\n";
}


##############
## dokuwiki ##

my($dokuwiki_dl);

my(%dokuwiki_podentities) =
    ( 'lt' => '<', 'gt' => "\xFF\xFF", 'amp' => '&', 'solid' => '/',
      'verbar' => '|', 'eq' => '=', 'star' => '*', 'lparen' => '(',
      'rparen' => ')', 'lbrack' => '[', 'rbrack' => ']', 'lbrace' => '{',
      'rbrace' => '}' );
my($dokuwiki_sensitive_char) = "[!%'\\(\\)\\*\\+/?\\[\\\\\\]_\\{\\}]";

sub dokuwiki_unentity ($) {
    my($x) = @_;
    if ($x =~ /^\d+$/) {
        $x = pack 'U', $x;
        if ($x =~ /\A$dokuwiki_sensitive_char\Z/) {
            "%%$x%%";
        } else {
            $x;
        }
    } elsif ($dokuwiki_podentities{$x}) {
        $dokuwiki_podentities{$x};
    } else {
        print STDERR "click-elem2man: $Filename: unknown entity E<$x>\n";
        "";
    }
}

sub dokuwiki_quotedbl ($) {
    my($x) = @_;
    $x =~ s/^(.)/\%\%$1\%\%/;
    $x;
}

sub dokuwiki_surround ($$) {
    my($text, $chr) = @_;
    my($re) = "\\$chr\\$chr";
    $text =~ s{$re}{}g;
    "$chr$chr$text$chr$chr";
}

sub dokuwikiize_text ($) {
    my($t) = @_;

    # embolden & manpageize
    $t = textize_add_links($t, 'P');

    # get rid of entities
    $t =~ s{E<([^>]*)>}{dokuwiki_unentity($1)}ge;
    $t =~ s{($dokuwiki_sensitive_char)}{^$1^}g;

    my $maxnest = 10;
    while ($maxnest-- && $t =~ /[A-Z]</) {

        # can't do C font here
        $t =~ s/B<($nonest)>/dokuwiki_surround($1, "*")/eg;
        $t =~ s.I<($nonest)>.dokuwiki_surround($1, "/").eg;
        $t =~ s.U<($nonest)>.dokuwiki_surround($1, "_").eg;
        $t =~ s/P<($nonest)>/$1/g;
        $t =~ s/C<($nonest)>/dokuwiki_surround($1, "'")/eg;

        # files and filelike refs in italics
        $t =~ s/F<($nonest)>/I<$1>/g;

        # LREF: man page references
        $t =~ s{L<($nonest)\^\(\^[\dln]\^\)\^>}{[[$1]]}g;
        $t =~ s{L<($nonest)>\^\(\^[\dln]\^\)\^}{[[$1]]}g;

        # LREF: a la HREF L<show this text|man/section>
        $t =~ s{L<($nonest)\|($nonest)>}{[[$2|$1]]}g;
        $t =~ s{L<($nonest)>}{[[$1]]}g;

        $t =~ s/V<($nonest)>//g;
        $t =~ s/Z<>//g;
        $t =~ s/N<>(\n?)/\\\\ /g;
    }

    $t =~ s/\n+/\n/sg;
    $t =~ s/^\n+//;
    $t =~ s/\n+$//;
    $t =~ s/\xFF\xFF/>/g;

    $t =~ s{^(\s*)\^([?!])\^}{$1 . dokuwiki_quotedbl($2)}egm;
    $t =~ s{($dokuwiki_sensitive_char)\^\1\^}{$1 . dokuwiki_quotedbl($1)}eg;
    $t =~ s{\^($dokuwiki_sensitive_char)\^\1}{dokuwiki_quotedbl($1) . $1}eg;
    $t =~ s{\^($dokuwiki_sensitive_char)\^}{$1}g;

    # remove self references
    $t =~ s{\[\[$PrimaryElement\]\]}{$PrimaryElement}g;

    $t;
}

sub dokuwikiize ($) {
    my($t) = @_;
    # $t cannot contain \t, as all \ts have been expanded to spaces below

    if ($t =~ /^ /m) {
        # worry about verbatims
        $t =~ s/^( .*)\n(\n+)(?= )/$1 . "\n" . (" \n" x length($2))/mge;
        my(@x) = split(/(^ .*$)/m, $t);
        my($o, $i) = '';
        for ($i = 0; $i < @x; $i += 2) {
            if ($x[$i]) {
                $o .= dokuwikiize_text($x[$i]);
                $o .= "\n" if $o !~ /\n\Z/;
            }
            if ($x[$i+1]) {
                $o .= "  " . $x[$i+1] . "\n";
            }
        }
        $o;
    } else {
        dokuwikiize_text($t);
    }
}

sub dokuwiki_do_section ($$$$) {
    my($name, $args, $text, $filename) = @_;
    if (!exists($xsection_takes_args{$name})) {
        print STDERR "click-elem2man: $Filename: unknown section '=$name' ignored\n";
        return;
    }
    print STDERR "click-elem2man: $Filename: section '=$name' requires arguments\n"
        if ($xsection_takes_args{$name} && !$args);

    # handle '=begin' .. '=end'
    if ($name eq 'end') {
        undef $Begun;
    } elsif ($Begun && $Begun eq 'dokuwiki') {
        print OUT '=', $name, ($args ? ' ' . $args : ''), "\n", $text;
        return;
    } elsif ($Begun) {
        return;
    }

    if ($name eq 'head1') {
        print OUT "\n===== ", dokuwikiize($args), " =====\n\n";
    } elsif ($name eq 'head2') {
        print OUT "\n==== ", dokuwikiize($args), " ====\n\n";
    } elsif ($name eq 'over') {
        if ($args =~ /^\s*(\d+)\s*$/s) {
            push @Over, $1;
        } else {
            print STDERR "click-elem2man: $Filename: bad arguments to '=over'\n";
        }
    } elsif ($name eq 'item') {
        my($nover) = scalar(@Over);
        $nover = 1 if $nover == 0;
        print OUT "\n", ' ' x $nover, "? " if $dokuwiki_dl;
        print OUT dokuwikiize("B<" . quote_unquoted_gt($args) . ">");
        print OUT ($dokuwiki_dl ? "\n" : "\n\n");
    } elsif ($name eq 'back') {
        my($overarg);
        if ($args =~ /^\s*(\d+)\s*$/s) {
            $overarg = $1;
        } elsif ($args !~ /^\s*$/s) {
            print STDERR "click-elem2man: $Filename: bad arguments to '=back'\n";
        }
        if (@Over == 0) {
            print STDERR "click-elem2man: $Filename: too many '=back's\n";
        } else {
            my($over) = pop @Over;
            print STDERR "click-elem2man: $Filename: '=back $overarg' paired with '=over $over'\n"
                if defined($overarg) && $over != $overarg;
        }
    } elsif ($name eq 'for') {
        my($fortext);
        if ($text =~ /^(.*)\n\s*\n(.*)$/s) {
            ($fortext, $text) = ($1, $2);
        } else {
            ($fortext, $text) = ($text, '');
        }
        if ($args =~ /^\s*dokuwiki\s*(.*)/) {
            print OUT $2, $fortext;
        }
    } elsif ($name eq 'begin') {
        $Begun = $args;
        $Begun =~ s/^\s*(\S+).*/$1/;
        if ($Begun eq 'dokuwiki') {
            print OUT $text;
        }
        return;
    } elsif ($name eq 'start!') {
        return;
    } elsif ($name eq 'name!') {
        print OUT <<"EOD;";
====== $args Element Documentation ======

===== NAME =====

**$args** -- $text
EOD;
        return;
    } elsif ($name eq 'end!') {
        print OUT "\n\nGenerated by 'click-elem2man' from '$Filename' on $today.\n";
        return;
    }

    print OUT ' ' x @Over, "! " if $text =~ /\S/ && @Over && $dokuwiki_dl;
    print OUT dokuwikiize($text), "\n";
    print OUT "\n" if $name eq 'head1';
}


##############
## Markdown ##

my(%markdown_podentities) =
    ( 'lt' => '\xFF', 'gt' => "\xFE", 'amp' => '&', 'solid' => '/',
      'verbar' => '|', 'eq' => '=', 'star' => '*', 'lparen' => '(',
      'rparen' => ')', 'lbrack' => '[', 'rbrack' => ']', 'lbrace' => '{',
      'rbrace' => '}' );
my($markdown_sensitive_char) = "[&\\[\\]#*_~`]";
my($markdown_was_short_item) = 0;

sub markdown_unentity ($) {
    my($x) = @_;
    if ($x =~ /^\d+$/) {
        $x = pack 'U', $x;
        if ($x =~ /\A$markdown_sensitive_char\Z/) {
            "&#" . ord($x) . ";";
        } else {
            $x;
        }
    } elsif ($markdown_podentities{$x}) {
        $markdown_podentities{$x};
    } else {
        print STDERR "click-elem2man: $Filename: unknown entity E<$x>\n";
        "";
    }
}

sub markdown_quote ($) {
    "&#" . ord($_[0]) . ";";
}

sub markdown_surround ($$$) {
    my($text, $start, $end) = @_;
    $start . $text . $end;
}

sub markdownize_text ($$) {
    my($t, $indent) = @_;

    # embolden & manpageize
    $t = textize_add_links($t, 'P');

    # get rid of entities
    $t =~ s{E<([^>]*)>}{markdown_unentity($1)}ge;
    $t =~ s{($markdown_sensitive_char)}{markdown_quote($1)}ge;

    my $maxnest = 10;
    while ($maxnest-- && $t =~ /[A-Z]</) {

        # can't do C font here
        $t =~ s{B<($nonest)>}{markdown_surround($1, "\xFFstrong\xFE", "\xFF/strong\xFE")}eg;
        $t =~ s{I<($nonest)>}{markdown_surround($1, "\xFFem\xFE", "\xFF/em\xFE")}eg;
        $t =~ s{U<($nonest)>}{markdown_surround($1, "\xFFu\xFE", "\xFF/u\xFE")}eg;
        $t =~ s{P<($nonest)>}{$1}g;
        $t =~ s{C<($nonest)>}{markdown_surround($1, "\xFFcode\xFE", "\xFF/code\xFE")}eg;

        # files and filelike refs in italics
        $t =~ s/F<($nonest)>/I<$1>/g;

        # LREF: man page references
        $t =~ s{L<($nonest)\([\dln]\)>}{[$1]($1)}g;
        $t =~ s{L<($nonest)>\([\dln]\)}{[$1]($1)}g;

        # LREF: a la HREF L<show this text|man/section>
        $t =~ s{L<($nonest)\|($nonest)>}{[$1]($2)}g;
        $t =~ s{L<($nonest)>}{[$1]($1)}g;

        $t =~ s/V<($nonest)>//g;
        $t =~ s/Z<>//g;
        $t =~ s{N<>(\n?)}{\xFFbr /\xFE\n}g;
    }

    $t =~ s/\n\n+/\n\n/sg;
    $t =~ s/\A\n+//s;
    $t =~ s/\n*\z//s;
    $t =~ s{\xFF}{<}g;
    $t =~ s{\xFE}{>}g;

    $t =~ s{^([>#=])}{markdown_quote($1)}egm;
    $t =~ s{^}{$indent}mg if defined($indent) && $indent ne "";

    # remove self references
    $t =~ s{\[$PrimaryElement\]\($PrimaryElement\)}{$PrimaryElement}g;

    # quote colons that look like emoji escapes, and numbers that look like lists
    $t =~ s{:(?=[-+_a-z0-9]+:)}{\\:}ig;
    $t =~ s{^(\d+)\. }{$1\\. }mg;

    $t;
}

sub markdownize ($$) {
    my($t, $indent) = @_;
    # $t cannot contain \t, as all \ts have been expanded to spaces below

    if ($t =~ /^ /m) {
        # worry about verbatims
        $t =~ s/^( .*)\n(\n+)(?= )/$1 . "\n" . (" \n" x length($2))/mge;
        my(@x) = split(/(^ .*$)/m, $t);
        my($o, $i) = '';
        for ($i = 0; $i < @x; $i += 2) {
            if ($x[$i] && $x[$i] ne "\n") {
                $o .= markdownize_text($x[$i], $indent);
                $o =~ s{\n*\z}{\n\n};
            }
            if ($x[$i+1]) {
                $o .= $indent . "    " . $x[$i+1] . "\n";
            }
        }
        $o;
    } else {
        markdownize_text($t, $indent);
    }
}

sub markdown_do_section ($$$$) {
    my($name, $args, $text, $filename) = @_;

    $linkname = $filename . '#L' . $Lineno;

    if (!exists($xsection_takes_args{$name})) {
        print STDERR "click-elem2man: $Filename: unknown section '=$name' ignored\n";
        return;
    }
    print STDERR "click-elem2man: $Filename: section '=$name' requires arguments\n"
        if ($xsection_takes_args{$name} && !$args);
    my($closer) = "\n\n";
    my($nover) = scalar(@Over);

    if ($markdown_was_short_item && $name ne 'item' && $name ne 'start!') {
        $markdown_was_short_item = 0;
        print OUT "\n";
    }

    # handle '=begin' .. '=end'
    if ($name eq 'end') {
        undef $Begun;
    } elsif ($Begun && $Begun eq 'markdown') {
        print OUT '=', $name, ($args ? ' ' . $args : ''), "\n", $text;
        return;
    } elsif ($Begun) {
        return;
    }

    if ($name eq 'head1') {
        $section_name = markdownize($args, "");
        print OUT "\n", $section_name, "\n", ("-" x length($section_name)), "\n\n";
    } elsif ($name eq 'head2') {
        print OUT "\n### ", markdownize($args, ""), "\n\n";
    } elsif ($name eq 'over') {
        if ($args =~ /^\s*(\d+)\s*$/s) {
            push @Over, $1;
            $nover += 1;
        } else {
            print STDERR "click-elem2man: $Filename: bad arguments to '=over'\n";
        }
    } elsif ($name eq 'item') {
        my($bullet) = ("    " x ($nover ? $nover - 1 : 0)) . "* ";
        my($itemtext) = markdownize($args, "");
        if (length($itemtext) <= 70) {
            my($is_short_item) = $text !~ /\S\n\n+\S/;
            print OUT "\n" if !$is_short_item && $markdown_was_short_item;
            print OUT $bullet, $itemtext, " —\n";
            $text =~ s{\A\n+}{};
            if ($is_short_item) {
                $text =~ s{\n+\z}{};
                $closer = "\n";
                $nover = 0;
                $markdown_was_short_item = 1;
            }
        } else {
            print OUT $bullet, $itemtext, "\n\n";
        }
    } elsif ($name eq 'back') {
        my($overarg);
        if ($args =~ /^\s*(\d+)\s*$/s) {
            $overarg = $1;
        } elsif ($args !~ /^\s*$/s) {
            print STDERR "click-elem2man: $Filename: bad arguments to '=back'\n";
        }
        if (@Over == 0) {
            print STDERR "click-elem2man: $Filename: too many '=back's\n";
        } else {
            my($over) = pop @Over;
            print STDERR "click-elem2man: $Filename: '=back $overarg' paired with '=over $over'\n"
                if defined($overarg) && $over != $overarg;
            $nover -= 1;
        }
    } elsif ($name eq 'for') {
        my($fortext);
        if ($text =~ /^(.*)\n\s*\n(.*)$/s) {
            ($fortext, $text) = ($1, $2);
        } else {
            ($fortext, $text) = ($text, '');
        }
        if ($args =~ /^\s*markdown\s*(.*)/) {
            print OUT $2, $fortext;
        }
    } elsif ($name eq 'begin') {
        $Begun = $args;
        $Begun =~ s/^\s*(\S+).*/$1/;
        if ($Begun eq 'markdown') {
            print OUT $text;
        }
        return;
    } elsif ($name eq 'start!') {
        $markdown_was_short_item = 0;
        return;
    } elsif ($name eq 'name!') {
        $s = "$args Element Documentation";
        print OUT $s, "\n", "=" x length($s), "\n\nNAME\n----\n\n",
          "**$args** — $text\n";
        return;
    } elsif ($name eq 'end!') {
        print OUT "\n\nGenerated by click-elem2man from <code>[$Filename]($Github/blob/master/doc/$linkname)</code> on $today.\n";
        return;
    }

    $text = markdownize($text, $nover ? "    " x $nover : "");
    $closer = "\n" if $text =~ m{<br />\z};
    print OUT $text, $closer;
}

sub markdown_elementlist ($@) {
    my($sec) = shift @_;
    my($t);
    if ($sec eq "Alphabetical") {
        $t = "Alphabetical list\n-----------------\n\n";
    } else {
        $t = "$sec\n" . ("-" x length($sec)) . "\n\n";
    }
    foreach $_ (sort { lc($a) cmp lc($b) } @_) {
        my($os) = $all_outsections{$_};
        $os =~ s/\A(\d)[a-z]+\z/$1/;
        $t .= "* **[$_]($_)**";
        $t .= " (deprecated)" if $deprecated{$_};
        if ($all_summaries{$_} =~ /\S/) {
            my($u) = $all_summaries{$_};
            $u =~ s{\s+\z}{}s;
            $t .= " — " . $u;
        }
        $t .= "\n";
        $el_generated{$_} = 1;
    }
    $t . "\n";
}



###########
## main  ##

my $do_section_func = \&nroff_do_section;
my $do_text_func = \&nroffize;
my $elementlist_func = \&nroff_elementlist;
my $filename_func;

sub canonicalize_section ($$) {
    my($s, $args) = @_;
    if ($s eq "io") {
        return ("head1", "INPUTS AND OUTPUTS");
    } elsif ($s eq "processing") {
        return ("head1", "PROCESSING TYPE");
    } elsif ($s eq "d") {
        return ("head1", "DESCRIPTION");
    } elsif ($s eq "n") {
        return ("head1", "NOTES");
    } elsif ($s eq "e") {
        return ("head1", "EXAMPLES");
    } elsif ($s eq "a") {
        return ("head1", "SEE ALSO");
    } elsif ($s eq "head" && $args =~ /\A\s*(\d+)\s*(.*)\z/s) {
        return ("head" . $1, $2);
    } elsif ($s eq "head") {
        return ("head1", $args);
    } else {
        return ($s, $args);
    }
}

sub do_section ($$$$) {
    my($name, $args, $text, $filename) = @_;
    my(@text) = split(/^(=\w.*)$/m, $text);
    push @text, '' if !@text;
    my($i);
    @Over = ();
    undef $Begun;
    for ($i = 0; $i < @text; ) {
        ($name, $args) = canonicalize_section($name, $args);
        &$do_section_func($name, $args, $text[$i], $filename);
        ($name, $args) = ($text[$i+1] =~ /=(\w+)\s*(.*)/)
            if ($i < @text - 1);
        $i += 2;
    }
    &$do_section_func('back', '', '', $filename) while @Over;
    print STDERR "click-elem2man: $Filename: '=begin' not closed by end of section\n"
        if $Begun;
}

sub process_processing ($) {
    my($t) = @_;
    return undef if !defined($t);
    if (exists($processing_constants{$t})) {
        $t = $processing_constants{$t};
    }
    $t =~ tr/\" \t//d;
    $t =~ s{\A([^/]*)\Z}{$1/$1};
    $processing_text{$t};
}

sub process_one_portcount ($$) {
    my($t, $type) = @_;
    if ($t eq '0') {
        "no ${type}s";
    } elsif ($t eq '-') {
        "any number of ${type}s";
    } elsif ($t eq '1') {
        "1 $type";
    } elsif ($t eq '=') {
        "the same number of ${type}s";
    } elsif ($t =~ /^=(\++)$/) {
        length($1) . " more $type";
    } elsif ($t =~ /^0?-1$/) {
        "at most 1 $type";
    } elsif ($t =~ /^0?-(.*)/) {
        "at most $1 ${type}s";
    } elsif ($t =~ /^(\d+)-$/) {
        "$1 or more ${type}s";
    } else {
        "$t ${type}s";
    }
}

sub process_portcount ($) {
    my($t) = @_;
    $t = "$t/$t" if $t !~ /\//;
    return 'none' if $t eq '0/0';
    my($i, $o) = split(/\//, $t);
    process_one_portcount($i, "input") . ", " . process_one_portcount($o, "output");
}

sub process_handler_type ($) {
    my($h) = @_;
    $h = "read-only" if $h eq 'r';
    $h = "write-only" if $h eq 'w';
    $h = "read/write" if $h eq 'rw';
    $h;
}

sub process_summary_section ($$) {
    my($summary, $file) = @_;
    my($i);
    foreach $i (split(/,/, $summary)) {
        $i =~ s/^\s*//;
        $i =~ s/\s*$//;
        $i = $section_headers{$i} if exists $section_headers{$i};
        next if !$i;
        push @{$summaries_by_section{$i}}, $file;
    }
}

sub process_drivers ($) {
    my($driv) = @_;
    my(@d, $d);
    foreach $d ('userlevel', 'linuxmodule', 'bsdmodule', 'ns') {
        push @d, $d if $driv =~ /\b$d\b/;
    }
    join(', ', @d);
}

sub insert_section (\@\@\@$$$$) {
    my($sn, $sa, $st, $i, $n, $a, $t) = @_;
    splice @$sn, $i, 0, $n;
    splice @$sa, $i, 0, $a;
    splice @$st, $i, 0, $t;
}

sub insert_section2 (\@\@\@$$$\%@) {
    my($sn, $sa, $st, $n, $a, $t, $fis, @x) = @_;
    my($pos);
    foreach $pos (@x) {
        if (exists $fis->{$pos}) {
            insert_section(@$sn, @$sa, @$st, $fis->{$pos} + 1, $n, $a, $t);
            $fis->{$n} = $fis->{$pos} + 1;
            return;
        }
    }
}

sub process_comment ($$$) {
    my($t, $filename, $lineno) = @_;
    my($i);
    $Filename = $filename . ":" . $lineno;

    # split document into sections
    my(@section_text, @section_args, @section_name, $bad_section, $ref);
    $ref = \$bad_section;
    while ($t =~ m{^=(\w+)( *)(.*)([\0-\377]*?)(?=^=\w|\Z)}mg) {
        if ($section_break{$1}) {
            insert_section(@section_name, @section_args, @section_text,
                           @section_name, $1, $3, $4);
            $ref = \$section_text[-1];
        } else {
            $$ref .= '=' . $1 . $2 . $3 . $4;
        }
    }

    # check document for sectioning errors
    print STDERR "click-elem2man: $Filename: warning: comment does not start with section\n"
        if $bad_section;
    my(%num_sections, %first_in_section);
    foreach $i (0..$#section_name) {
        my($n) = $section_name[$i];
        print STDERR "click-elem2man: $Filename: warning: section '=$n' requires arguments\n"
            if $section_takes_args{$n} == 1 && !$section_args[$i];
        print STDERR "click-elem2man: $Filename: warning: section '=$n' arguments ignored\n"
            if $section_takes_args{$n} == 0 && $section_args[$i];
        print STDERR "click-elem2man: $Filename: warning: empty section '=$n'\n"
            if $section_takes_text{$n} == 1 && !$section_text[$i];
        print STDERR "click-elem2man: $Filename: warning: section '=$n' text ignored\n"
            if $section_takes_text{$n} == 0 && $section_text[$i] =~ /\S/;
        $num_sections{$n}++;
        $first_in_section{$n} = $i if $num_sections{$n} == 1;
    }
    foreach $i ('a', 'c', 'd', 'n', 'e', 'title', 'io', 'processing', 'drivers', 'batching') {
        print STDERR "click-elem2man: $Filename: warning: multiple '=$i' sections; some may be ignored\n"
            if $num_sections{$i} && $num_sections{$i} > 1;
    }

    # read class names from configuration arguments section
    $i = $first_in_section{'c'};
    if (!defined($i)) {
        print STDERR "click-elem2man: $Filename: section '=c' missing; cannot continue\n";
        return;
    }
    my(@classes, %classes);
    while ($section_text[$i] =~ /^\s*(\w+)\(/mg) {
        push @classes, $1 if !exists $classes{$1};
        $classes{$1} = 1;
    }
    if (!@classes && $section_text[$i] =~ /^\s*([\w@]+)\s*$/) {
        push @classes, $1;
        $classes{$1} = 1;
    }
    if (!@classes) {
        print STDERR "click-elem2man: $Filename: no class definitions\n    (did you forget '()' in the =c section?)\n";
        return;
    }
    my($Title) = $classes[0];

    # output filenames might be specified in 'title' section
    my(@outfiles, @outsections, $title);
    if (defined($first_in_section{'title'})) {
        $title = $section_args[ $first_in_section{'title'} ];
        if (!$title) {
            print STDERR "click-elem2man: $Filename: '=title' section present, but empty\n";
            return;
        }
        if ($title =~ /[^-.\w@+,]/) {
            print STDERR "click-elem2man: $Filename: strange characters in '=title', aborting\n";
            return;
        }
        foreach $i (split(/\s+/, $title)) {
            if ($i =~ /^(.*)\((.*)\)$/) {
                push @outfiles, $1;
                push @outsections, $2;
                $Title = $1;
            } else {
                push @outfiles, $i;
                push @outsections, $man_section;
                $Title = $i;
            }
        }
    } else {
        $title = join(', ', @classes);
        @outfiles = @classes;
        @outsections = ($man_section) x @classes;
    }

    # open new output file if necessary
    my($main_outname);
    if ($filename_func) {
        # maybe clean up old installation names
        my(@killfiles);
        for ($i = 0; $i != @outfiles; ++$i) {
            push @killfiles, &$filename_func($outfiles[$i], $outsections[$i], 1);
        }
        foreach $i (@killfiles) {
            print STDERR "Removing $i\n" if $verbose && -f $i;
            unlink($i);
        }

        # install main file
        $main_outname = &$filename_func($outfiles[0], $outsections[0], 0);
        if ($uninstall) {
            print STDERR "Uninstalled $main_outname\n" if $verbose;
            unlink($main_outname);
            open(OUT, ">/dev/null");
        } elsif ($gzip ? open(OUT, "|-", "gzip -9 >'$main_outname~'") : open(OUT, ">", $main_outname . "~")) {
            print STDERR "Writing to $main_outname\n" if $verbose;
        } else {
            print STDERR "$main_outname: $!\n";
            return;
        }
    }
    push @all_outfiles, $outfiles[0];
    $all_outsections{$outfiles[0]} = $outsections[0];
    $PrimaryElement = $outfiles[0];

    # prepare related
    %RelatedSource = ();
    $i = $first_in_section{'a'};
    if (defined($i)) {
        $section_text[$i] = $section_args[$i] . $section_text[$i]
            if $section_args[$i];
        if ($section_text[$i] =~ /\A\s*(.*?)(\n\s*\n.*\Z|\Z)/s) {
            my($bit, $last) = ($1, $2);
            while ($bit =~ m{(\b[A-Z][-\w@.+=]+)([,\s]|\Z)}g) {
                $RelatedSource{$1} = $man_linksection;
            }
            $bit =~ s{([-\w@.+=]+)([,\s]|\Z)}{$1($man_linksection)$2}g;
            while ($bit =~ m{([-\w@.+=]+\(([0-9ln])\))}g) {
                $RelatedSource{$1} = $2;
            }
            $section_text[$i] = $bit . $last;
        }
    }
    map(delete $RelatedSource{$_}, @outfiles);
    @Related = sort { length($b) <=> length($a) } (keys %RelatedSource, @classes);

    # front matter
    my($oneliner) = (@classes == 1 ? "Click element" : "Click elements");
    $all_summaries{$outfiles[0]} = '';
    $i = $first_in_section{'s'};
    my($summary_section) = '';
    if (defined($i)) {
        $summary_section = $section_args[$i];
        process_summary_section($summary_section, $outfiles[0]);
        $section_text[$i] =~ s/\n\s*\n/\n/g;
        my($t) = &$do_text_func($section_text[$i]);
        $oneliner .= ";\n" . $t;
        $oneliner =~ s/\n(^\.)/ /g;
        if ($t ne "") {
            $all_summaries{$outfiles[0]} = $t . "\n";
        }
    }

    # deprecation
    if (defined($first_in_section{'deprecated'})) {
        $deprecated{$outfiles[0]} = $section_text[$first_in_section{'deprecated'}];
    }

    # package
    if (!defined($first_in_section{'package'}) && defined($package)) { PACKAGE: {
        my($pkg) = $package;
        if ($pkg eq 'DEFAULT') {
            $pkg = $1 if $Filename =~ m|\belements/(\w+)/|;
            $pkg = 'standard' if $Filename =~ m|\binclude/click/standard/|;
            last PACKAGE if $pkg eq 'DEFAULT';
            $pkg = exists($default_packages{$pkg}) ? $default_packages{$pkg} : $pkg;
        }
        insert_section2(@section_name, @section_args, @section_text,
                        'package', '', $pkg,
                        %first_in_section, 'drivers', 'processing', 'io', 'batching', 'c');
    }}

    # drivers
    my($drivers);
    if ($docrequires{$Title}
        && ($drivers = process_drivers($docrequires{$Title}))
        && !defined($first_in_section{'drivers'})) {
        insert_section2(@section_name, @section_args, @section_text,
                        'drivers', '', $drivers,
                        %first_in_section, 'processing', 'batching', 'io', 'c');
    }

    # processing
    if (!defined($first_in_section{'processing'})) { PROCESSING: {
        # can we figure out the processing type?
        last PROCESSING
            if (defined($first_in_section{'io'}) && $section_text[$first_in_section{'io'}] =~ /None/i) || (defined($portcount{$Title}) && ($portcount{$Title} eq '0' || $portcount{$Title} eq '0/0'));
        my($ptype) = process_processing($processing{$Title});
        insert_section2(@section_name, @section_args, @section_text,
                        'processing', '', $ptype,
                        %first_in_section, 'io', 'c') if $ptype;
    }}

    # input/output
    if (!defined($first_in_section{'io'})) { PORTCOUNT: {
        last PORTCOUNT if !defined($portcount{$Title});
        insert_section2(@section_name, @section_args, @section_text,
                        'io', '', process_portcount($portcount{$Title}),
                        %first_in_section, 'c');
    }}

    # batching
    if (!defined($first_in_section{'batching'})) { BATCHING: {
        last BATCHING if !defined($batching{$Title});
        insert_section2(@section_name, @section_args, @section_text,
                        'batching', '', 'Batching natively supported',
                        %first_in_section, 'c');
    }}

    # initial sections
    insert_section(@section_name, @section_args, @section_text,
                   0, 'start!', $outsections[0], $title);
    insert_section(@section_name, @section_args, @section_text,
                   1, 'name!', $title, $oneliner);
    insert_section(@section_name, @section_args, @section_text,
                   @section_name, 'end!', $outsections[0], $title);

    # output
    my($special_section) = 0;
    for ($i = 0; $i < @section_text; $i++) {
        my($s) = $section_name[$i];
        my($x) = $section_text[$i];
        my($was_special_section) = $special_section;
        $special_section = 0;

        if ($s eq 'c') {
            $x =~ s{(\S\s*)\n}{$1N<>\n}g;
            $x =~ s{N<>\n*\z}{};
            do_section('head1', 'SYNOPSIS', $x, $filename);
        } elsif ($s eq 'package' || $s eq 'io' || $s eq 'processing' || $s eq 'batching'
                 || $s eq 'drivers') {
            $x =~ s/\A\s+//;
            $x =~ s/\s+\Z//;
            do_section('text', '', 'B<' . $infosection_name{$s} . '>: ' . $x . 'N<>', $filename);
        } elsif ($s eq 'h') {
            my($t) = "=over 5\n";
            while ($i < @section_text && $section_name[$i] eq 'h') {
                if ($section_args[$i] =~ /\A\s*(.*?)\s*"(.*?)"\s*\Z/
                    || $section_args[$i] =~ /\A\s*(.*?)\s*(\S+)\s*\Z/) {
                    $t .= "=item B<" . ($1 eq "" ? $2 : $1) . ">";
                    if ($1 ne "") {
                        $t .= " (" . process_handler_type($2) . ")";
                    }
                    $t .= "\n";
                } else {
                    print STDERR "click-elem2man: $Filename: bad handler section arguments ('=h $section_args[$i]')\n";
                    $t .= "=item B<$section_args[$i]>\n";
                }
                $t .= $section_text[$i] . "\n";
                $i++;
            }
            $i--;
            do_section('head1', 'ELEMENT HANDLERS', $t, $filename);
        } elsif ($s eq 'title' || $s eq 's' || $s eq 'deprecated') {
            # nada
        } else {
            do_section($s, $section_args[$i], $x, $filename);
        }
    }

    # close output file & make links if appropriate
    if ($filename_func) {
        close OUT;
        sub check_exists_command {
           my $check = `sh -c 'command -v $_[0]'`;
            return $check;
        }
        $zcat = check_exists_command("gzcat") ? "gzcat" : "zcat";
        if ($gzip ? open(INX, "-|", $zcat, $main_outname) : open(INX, "<", $main_outname)) {
            $gzip ? open(OUTX, "-|", $zcat, $main_outname . "~") : open(OUTX, "<", $main_outname . "~");
            local($/) = undef;
            my($in) = <INX>;
            my($out) = <OUTX>;
            close INX;
            close OUTX;
            if (defined($in) && defined($out) && $in ne "" && $out ne "") {
                $in =~ s{\nGenerated by click-elem2man[^\n]*\n+\z}{};
                $out =~ s{\nGenerated by click-elem2man[^\n]*\n+\z}{};
                if ($in eq $out) {
                    unlink($main_outname . "~");
                    print STDERR "No changes to $main_outname\n" if $verbose;
                    return;
                }
            }
        }

        rename("$main_outname~", $main_outname);

        for ($i = 1; $i < @outfiles; $i++) {
            my($outname) = &$filename_func($outfiles[$i], $outsections[$i], 0);
            print STDERR "Unlinking $outname\n" if $verbose;
            unlink($outname);
            if ($uninstall) {
                # do nothing
            } elsif (link $main_outname, $outname) {
                print STDERR "Linked $outname\n" if $verbose;
                push @all_outfiles, $outfiles[$i];
                $all_outsections{$outfiles[$i]} = $outsections[$i];
                process_summary_section($summary_section, $outfiles[$i]);
                $all_summaries{$outfiles[$i]} = $all_summaries{$outfiles[0]};
            } else {
                print STDERR "click-elem2man: $outname: $!\n";
            }
        }
    }
}

sub process_file ($;$) {
    my($filename, $text) = @_;

    print STDERR "Attempting $filename...\n" if $verbose;
    if ($filename ne "<stdin>") {
        $filename = "include/$1" if !-e $filename && $filename =~ /^<(.*)>$/s;
        $filename = "$PREFIX/$filename" if !-e $filename && defined($PREFIX);
        if (!-e $filename) {
            print STDERR "$filename: Not found\n";
            return;
        }
    }
    print STDERR "Found $filename\n" if $verbose;
    if (!defined($text)) {
        if (!open(IN, $filename)) {
            print STDERR "click-elem2man: $filename: $!\n";
            return;
        }
        $text = <IN>;
        close IN;
    }

    my($x, $t, $p, $lineno);
    $lineno = 1;
    print "$filename??\n" if !defined $text;
    foreach $_ (split(m{(/\*.*?\*/)}s, $text)) {
        my($last_lineno) = $lineno;
        $lineno += ($_ =~ tr/\n/\n/);
        if (/^\/\*/m && /^[\/*\s]+=[a-z]/m) {
            s/^(.*?)\t/$1 . (' ' x (8 - (length($1) % 8)))/egm while /\t/;
            s/^\/\*\s*//g;
            s/\s*\*\/$//g;
            if (/^ ?\*/m) {
                $p = undef;
                $t = "";
                foreach $x (split("\n", $_)) {
                    $x =~ s/\s*\z//;
                    if (!defined($p) && $x =~ /\A ?\*( *)=/s) {
                        $p = $1;
                    }
                    if (defined($p) && $x =~ /\A ?\*( *)(.*)\z/s) {
                        $x = (length($1) <= length($p) ? "" : $p) . $2;
                    } elsif ($x =~ /\A ?\* ?(.*)\z/s) {
                        $x = $1;
                    }
                    $t .= $x . "\n";
                }
                $_ = $t;
            }
            $Lineno = $lineno;
            process_comment($_, $filename, $last_lineno);
        }
    }
}

sub xmlunquote ($) {
    my($x) = @_;
    if ($x =~ /&/) {
        $x =~ s/&lt;/</g;
        $x =~ s/&gt;/>/g;
        $x =~ s/&amp;/&/g;
        $x =~ s/&#([0-9]+);/chr($1)/eg;
        $x =~ s/&#x([0-9a-zA-Z]+);/chr(oct("0x$1"))/eg;
    }
    $x;
}

sub read_elementmap_text ($;$) {
    my($text, $headerhash) = @_;
    $headerhash = {} if !defined($headerhash);
    local($_);
    foreach $_ (split(/\n+/, $text)) {
        next if !/^<entry\b/;
        my($n) = (/\bname="(.*?)"/);
        my($dn) = (/\bdocname="(.*?)"/);
        my($p) = (/\bprocessing="(.*?)"/);
        my($hf) = (/\bheaderfile="(.*?)"/);
        my($b) = (/\bbatching="(.*?)"/);
        my($pc) = (/\bportcount="(.*?)"/);
        my($req) = (/\brequires="(.*?)"/);
        if (defined($n) || defined($dn)) {
            $dn = xmlunquote(defined($dn) ? $dn : $n);
            $processing{$dn} = xmlunquote($p) if defined($p);
            $portcount{$dn} = xmlunquote($pc) if defined($pc);
            $batching{$dn} = xmlunquote($b) if defined($b);
            $req = defined($req) ? xmlunquote($req) : undef;
            $docrequires{$dn} = $req if defined($req);

            $n = $1 if defined($req) && !defined($n) && /\bprovides="(.*?)"/;
            if (defined($n) && defined($req)) {
                $n = xmlunquote($n);
                $requires{$n} = [] if !defined $requires{$n};
                push @{$requires{$n}}, $req;
            }

            $headerhash->{xmlunquote($hf)} = 1 if defined($hf);
        }
    }
}

sub read_elementmap ($;$) {
    my($fn, $headerhash) = @_;
    open(E, $fn) || die "$fn: $!\n";
    local($/) = undef;
    read_elementmap_text(<E>, $headerhash);
    close E;
}

sub process_file_set ($$\@) {
    my($fn, $fnhash, $filedata) = @_;
    if (open(IN, ($fn eq '-' ? "<&STDIN" : $fn))) {
        my(@a, @b, $t, $x);
        $t = <IN>;
        close IN;

        # Parse file; click-buildtool gets special treatment
        if ($t =~ /\A#.*click-buildtool findelem/) {
            $t =~ s/^#.*//mg;
            foreach $_ (split(/\n+/, $t)) {
                if (/^\S+\s+<(\S+?)>/) {
                    $fnhash->{"include/$1"} = 1;
                } elsif (/^\S+\s+"(\S+?)"/) {
                    $fnhash->{$1} = 1;
                } else {
                    s/.cc$/.hh/;
                    $fnhash->{$_} = 1;
                }
            }
        } elsif ($t =~ /\A<\?xml/) {
            read_elementmap_text($t, $fnhash);
        } elsif ($t =~ /[\(\)]/) {
            if ($fn ne "-") {
                $fnhash->{$fn} = 1;
            } else {
                push @$filedata, $t;
            }
        } else {
            $t =~ s/^#.*//mg;
            foreach $_ (split(/\s+/, $t)) {
                if ($t =~ /[*?\[]/) {
                    foreach my $x (glob($t)) {
                        $fnhash->{$x} = 1;
                    }
                } else {
                    $fnhash->{$_} = 1;
                }
            }
        }
    } else {
        print STDERR "click-elem2man: $fn: $!\n";
    }
}

sub uniq (@) {
    my(@a, $x);
    foreach $x (@_) {
        push @a, $x if !@a || $a[-1] ne $x;
    }
    @a;
}


sub driver_requirement ($) {
    my($req) = @_;
    my($d) = 0;
    $d |= 1 if $req =~ /\buserlevel\b/;
    $d |= 2 if $req =~ /\blinuxmodule\b/;
    $d |= 4 if $req =~ /\bbsdmodule\b/;
    $d |= 8 if $req =~ /\bns\b/;
    $d ? $d : 15;
}

my(%requires_fixed);
sub fix_one_requires ($) {
    my($req) = @_;
    my($driver) = driver_requirement($req);

    my($t, $i);
    my($changes) = '';
    foreach $t (split(/\s+/, $req)) {
        if (defined($requires{$t})) {
            my($treq) = $requires{$t};
            if (!$requires_fixed{$t}) {
                $requires_fixed{$t} = 1;
                for ($i = 0; $i < @$treq; $i++) {
                    $treq->[$i] = &fix_one_requires($treq->[$i]);
                }
            }
            for ($i = 0; $i < @$treq; $i++) {
                if ($driver & driver_requirement($treq->[$i])) {
                    $changes .= " " . $treq->[$i];
                }
            }
        }
    }

    if ($changes) {
        join(' ', uniq(sort {$a cmp $b} split(/\s+/, $req . $changes)));
    } else {
        $req;
    }
}

sub fix_requires () {
    my($k);
    foreach $k (keys %docrequires) {
        $docrequires{$k} = fix_one_requires($docrequires{$k});
    }
}


# main program: parse options

sub usage () {
    print STDERR "Usage: click-elem2man [OPTIONS] FILES...
Try 'click-elem2man --help' for more information.\n";
    exit 1;
}

sub help () {
    print STDERR <<"EOD;";
'Click-elem2man' translates Click element documentation into manual pages.

Usage: click-elem2man [-l | -L] [-d DIRECTORY] FILE...

Each FILE is a Click header file, a list of Click header files, or the
output of click-mkelemmap.  '-' means standard input.

Options:
  -f, --files FILE        Read header filenames from FILE.
  -e, --elementmap EMAP   Read information about other elements from EMAP.
  -d, --directory DIR     Place generated files in directory DIR.
  -g, --github PATH       URL of a Github repository.
  -m, --man MANDIR        Place generated man pages under MANDIR/manSEC.
      --no-gzip           Do not gzip generated man pages.
  -P, --package PKG       Elements are in PKG package, or say 'DEFAULT'.
  -l, --list              Generate the elements(n) manual page as well.
  -L, --extend-list       Extend an existing elements(n) manual page.
  -p, --prefix PFX        Look for header files in PFX after looking in '.'.
      --markdown          Generate GitHub-flavored Markdown, not manual pages.
      --dokuwiki          Generate dokuwiki source, not manual pages.
      --dokuwiki-dl       Generate dokuwiki source using dl plugin.
  -u, --uninstall         Remove existing manual pages.
  -h, --help              Print this message and exit.

Report bugs to <https://github.com/kohler/click>.
EOD;
    exit 0;
}

undef $/;
my(@files, $fn, $elementlist, $any_files, $directory, $output_type, $mandir);
$output_type = 'nroff';

while (@ARGV) {
    $_ = shift @ARGV;
    if (/^-d$/ || /^--directory$/) {
        usage() if !@ARGV;
        $directory = shift @ARGV;
    } elsif (/^--directory=(.*)$/) {
        $directory = $1;
    } elsif (/^-m$/ || /^--man$/) {
        usage() if !@ARGV;
        $directory = shift @ARGV;
        $mandir = 1;
    } elsif (/^--man=(.*)$/) {
        $directory = $1;
        $mandir = 1;
    } elsif (/^-g$/ || /^--github$/) {
        usage() if !@ARGV;
        $Github = shift @ARGV;
    } elsif (/^--github=(.*)$/) {
        $Github = $1;
    } elsif (/^-P$/ || /^--package$/) {
        usage() if !@ARGV;
        $package = shift @ARGV;
    } elsif (/^-V$/ || /^--verbose$/) {
        $verbose = 1;
    } elsif (/^--no-gzip$/) {
        $gzip = "";
    } elsif (/^--package=(.*)$/) {
        $package = $1;
    } elsif (/^-f$/ || /^--files$/) {
        usage() if !@ARGV;
        push @files, shift @ARGV;
        $any_files = 1;
    } elsif (/^--files=(.*)$/) {
        push @files, $1;
        $any_files = 1;
    } elsif (/^-l$/ || /^--list$/) {
        $elementlist = 1;
    } elsif (/^-L$/ || /^--extend-list$/) {
        $elementlist = 2;
    } elsif (/^-e$/ || /^--elementmap$/) {
        usage() if !@ARGV;
        read_elementmap(shift @ARGV);
    } elsif (/^--elementmap=(.*)$/) {
        read_elementmap($1);
    } elsif (/^-p$/ || /^--prefix$/) {
        usage() if !@ARGV;
        $PREFIX = shift @ARGV;
    } elsif (/^--prefix=(.*)$/) {
        $PREFIX = $1;
    } elsif (/^-u$/ || /^--uninstall$/) {
        $uninstall = 1;
    } elsif (/^-h$/ || /^--help$/) {
        help();
    } elsif (/^--md$/ || /^--gfm$/ || /^--markdown$/) {
        $do_section_func = \&markdown_do_section;
        $do_text_func = \&markdownize;
        $elementlist_func = \&markdown_elementlist;
        $output_type = 'markdown';
    } elsif (/^--dokuwiki$/ || /^--doku$/) {
        $do_section_func = \&dokuwiki_do_section;
        $do_text_func = \&dokuwikiize;
        $output_type = 'dokuwiki';
    } elsif (/^--dokuwiki-dl$/) {
        $do_section_func = \&dokuwiki_do_section;
        $do_text_func = \&dokuwikiize;
        $output_type = 'dokuwiki';
        $dokuwiki_dl = 1;
    } elsif (/^-./) {
        usage();
    } elsif (/^-$/) {
        push @files, "-";
        $any_files = 1;
    } else {
        push @files, glob($_);
        $any_files = 1;
    }
}
push @files, "-" if !$any_files;

my(%man_subdirs);
sub nroff_filename_func ($$$) {
    my($name, $sec, $destroy) = @_;
    if ($destroy && $mandir) {
        ("$directory/mann/$name.n");
    } elsif ($destroy) {
        ();
    } elsif ($mandir) {
        my($subdir) = substr($sec, 0, 1);
        if (!exists($man_subdirs{$subdir}) && !-d "$directory/man$subdir") {
            print STDERR "Creating $directory/man$subdir\n" if $verbose;
            mkdir("$directory/man$subdir") || die "$directory/man$subdir: $!\n";
        }
        $man_subdirs{$subdir} = 1;
        "$directory/man$subdir/$name.$sec$gzip";
    } else {
        "$directory/$name.$sec$gzip";
    }
}

sub dokuwiki_filename_func ($$$) {
    my($name, $sec, $destroy) = @_;
    if ($destroy) {
        ();
    } elsif ($sec eq $man_section) {
        "$directory/" . lc($name) . ".txt$gzip";
    } else {
        "$directory/" . lc($name) . "-$sec.txt$gzip";
    }
}

sub markdown_filename_func ($$$) {
    my($name, $sec, $destroy) = @_;
    if ($destroy) {
        ();
    } elsif ($sec eq $man_section) {
        if ($name eq "elements") {
            return "$directory/Elements.md$gzip";
        } elsif ($name =~ /^elements-(.*)$/) {
            return "$directory/" . ucfirst($1) . "-Elements.md$gzip";
        } else {
            return "$directory/" . $name . ".md$gzip";
        }
    } else {
        return "$directory/" . $name . "-$sec.md$gzip";
    }
}

if ($directory && $output_type eq "markdown") {
    $filename_func = \&markdown_filename_func;
} elsif ($directory && $output_type eq "dokuwiki") {
    $filename_func = \&dokuwiki_filename_func;
} elsif ($directory) {
    $filename_func = \&nroff_filename_func;
}
if (!defined($gzip)) {
    $gzip = $output_type eq "nroff" ? ".gz" : "";
}

umask(022);
if ($uninstall) {
    open(OUT, ">/dev/null");
} elsif (!$directory) {
    open(OUT, ">&STDOUT");
}

# read file sets
my(%fnhash, @filedata);
foreach $fn (@files) {
    process_file_set($fn, \%fnhash, @filedata);
}
fix_requires();

# process files
foreach my $fn (keys(%fnhash)) {
    process_file($fn);
}
foreach my $text (@filedata) {
    process_file("<stdin>", $text);
}

if ($uninstall || !$directory) {
    close OUT;
}


# printing element list
#

sub read_elementlist ($) {
    my($fn) = @_;

    local($/) = "\n";
    if (!open(IN, $fn)) {
        print STDERR "click-elem2man: $fn: $!\n";
        return;
    }

    my($section, $active, $cur_name, $cur_section, %new_summary);
    $active = 0;
    while (<IN>) {
        if (/^\.SS \"(.*)\"/) {
            $section = $1;
        } elsif (/^\.TP 20/) {
            $active = 1;
        } elsif ($active == 1 && /^\.M (\S+) (\S+)/) {
            if (!exists $all_outsections{$1}) {
                $active = 2;
                $cur_name = $1;
                $new_summary{$1} = 1;
                $all_outsections{$1} = $2;
                $all_summaries{$1} = '';
                push @{$summaries_by_section{$section}}, $1;
                push @all_outfiles, $1;
            } elsif ($new_summary{$1}) {
                push @{$summaries_by_section{$section}}, $1;
                $active = 3;
            } else {
                $active = 3;
            }
        } elsif (/^\.PD/) {
            $active = 0;
        } elsif (/^\.SH \"ALPHABETICAL/) {
            last;
        } elsif ($active == 2) {
            $all_summaries{$cur_name} .= $_;
        }
    }
}

my(@Links, $Text, %unknown_shorthand);

sub one_summary ($@) {
    my($name, @elts) = @_;
    if ($name =~ /^[a-z]/) {
        my(@badelts);
        foreach my $x (@elts) {
            if (!exists($unknown_shorthand{"$x $name"})) {
                $unknown_shorthand{"$x $name"} = 1;
                push @badelts, $x;
            }
        }
        if (@badelts) {
            print STDERR "click-elem2man: warning: unknown category shorthand '$name' in ", join(", ", @badelts), "\n";
        }
    }
    my($a) = $name;
    if ($output_type eq "markdown") {
        $a =~ s{[^a-zA-Z0-9_]+}{-}g;
        $a = lc($a);
    } else {
        $a =~ s{([+\&\#\"\000-\037\177-\377])}{sprintf("%%%02X", ord($1))}eg;
        $a =~ tr/ /+/;
    }
    my($x) = $name;
    $x =~ s{ }{&nbsp;}g;
    if (@elts) {
        push @Links, "<a href=\"#$a\">$x</a>";
        $Text .= &$elementlist_func($name, @elts);
    }
}

sub write_elementlist ($$) {
    my($enamebase, $l_uninstall) = @_;
    if ($filename_func) {
        foreach my $fn (&$filename_func($enamebase, $man_section, 1)) {
            print STDERR "Removing $fn\n" if $verbose && -f $fn;
            unlink($fn);
        }

        my($fn) = &$filename_func($enamebase, $man_section, 0);
        if ($l_uninstall) {
            print STDERR "Removing $fn\n" if $verbose;
            unlink($fn);
            return;
        }
        if (!open(OUT, ">$fn")) {
            print STDERR "click-elem2man: $fn: $!\n";
            return;
        }
    } elsif ($uninstall) {
        return;
    }

    if ($output_type eq "markdown") {
        my($title);
        if ($enamebase eq "elements") {
            $title = "Elements";
        } elsif ($enamebase eq "elements-click") {
            $title = "Click Elements";
        } elsif ($enamebase =~ /^elements-(.*)$/) {
            $title = ucfirst($1) . " Package Elements";
        } else {
            $title = ucfirst($title);
        }
        print OUT $title, "\n", ("=" x length($title)), "\n\n",
            "This page lists Click element classes that have manual page documentation.\n\n";
    } else {
        print OUT <<"EOD;";
.\\" -*- mode: nroff -*-
.\\" Generated by 'click-elem2man'
$nroff_prologue
.TH "\U$enamebase\E" $man_section "$today" "Click"
.SH "NAME"
$enamebase \- documented Click element classes
.SH "DESCRIPTION"
This page lists all Click element classes that have manual page documentation.
EOD;
    }

    $Text = "";
    %el_generated = ();
    my(%did_section, $sec);
    # erase 'Miscellaneous' section, if any
    delete $summaries_by_section{'Miscellaneous'};
    my($i) = 0;
    foreach $sec ((map { $i++ % 2 ? ($_) : () } @section_headers),
                  (sort { lc($a) cmp lc($b) } keys(%summaries_by_section))) {
        next if $did_section{$sec};
        one_summary($sec, @{$summaries_by_section{$sec}});
        $did_section{$sec} = 1;
    }
    one_summary('Miscellaneous', grep { !$el_generated{$_} } @all_outfiles);

    my($links) = join("&nbsp;- ", @Links);
    if ($output_type eq "markdown") {
        print OUT "**By function:** ", $links, "\n\n";
        print OUT "**[Alphabetical list](#alphabetical-list)**\n\n";
    } else {
        print OUT <<"EOD;";
.\\"html <p><a href="#BY+FUNCTION"><b>By Function</b></a>:
.\\"html $links<br>
.\\"html <a href="#ALPHABETICAL+LIST"><b>Alphabetical List</b></a></p>
.SH \"BY FUNCTION\"
EOD;
    }

    print OUT $Text;
    print OUT &$elementlist_func("Alphabetical", @all_outfiles);

    close OUT if $filename_func;
}

sub read_main_elementlist () {
    if ($filename_func) {
        my($fn) = &$filename_func('elements', $man_section, 0);
        $fn = &$filename_func('elements-click', $man_section, 0) if !-r $fn;
        read_elementlist($fn) if -r $fn && $elementlist > 1;
    }
}

if ($elementlist && @all_outfiles) {
    # erase record of elements added on uninstall
    if ($elementlist > 1 && $uninstall) {
        %summaries_by_section = ();
        @all_outfiles = ();
    }

    # package-specific elementlist
    if (defined($package)) {
        my($fename) = ($package eq "DEFAULT" ? "elements-click" : "elements-$package");
        write_elementlist($fename, $uninstall);
    }
    read_main_elementlist();
    write_elementlist("elements", $uninstall && defined($package) && $package eq "DEFAULT");
}
